/*! ******************************************************************************
 *
 * Created on 2022年2月25日
 *
 * Copyright(c) YuZhou Big Data Laboratory CO.,Ltd, 2022.  All rights reserved.
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ******************************************************************************/

package org.yzbdl.lanius.core.net;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.*;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.*;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.*;
import cn.hutool.http.cookie.GlobalCookieManager;
import cn.hutool.http.ssl.SSLSocketFactoryBuilder;
import cn.hutool.system.OsInfo;
import org.pentaho.di.version.BuildVersion;
import org.yzbdl.lanius.authorize.verification.jar.JarValidate;
import org.yzbdl.lanius.core.exception.LaniusServerErrorException;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CookieManager;
import java.net.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * @author hs@yzbdl.ac.cn
 */
public class HttpRequest extends HttpBase<HttpRequest> {

    // header
    public static final String HEADER_MACHINE_CODE = "machine-code";

    public static final String HEADER_XREQUEST_WITH = "X-Requested-With";

    public static final String HEADER_CHARSET = "Charset";

    public static final String HEADER_ACCESS_TOKEN = "Authorization";

    public static final String HEADER_SYSTEM = "System";

    public static final String HEADER_LANIUS_VERSION = "ClientVersion";

    // value
    public static final String DEF_ACCEPT = "*.*";

    public static final String DEF_ACCEPT_ENCODING = "gzip, deflate, br";

    public static final String DEF_ACCEPT_LANGUAGE = "zh-CN,zh;q=0.9";

    public static final String DEF_AGENT = "LaniusDesign";

    public static final String DEF_MACHINE_CODE = JarValidate.MACHINE_CODE;

    public static final String DEF_SYSTEM = new OsInfo().getName();

    public static final String DEF_XREQUEST_WITH = "XMLHttpRequest";

    public static final String DEF_CHARSET = Charset.defaultCharset().toString();

    public static final String DEF_CACHE_CONTROL = "max-age=0";

    //
    private static final String BOUNDARY = "--------------------Lanius_" + RandomUtil.randomString(16);

    private static final byte[] BOUNDARY_END = StrUtil.format("--{}--\r\n", HttpRequest.BOUNDARY).getBytes();

    private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";

    private static final String CONTENT_DISPOSITION_FILE_TEMPLATE =
            "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";

    //
    private static final String CONTENT_TYPE_MULTIPART_PREFIX = "multipart/form-data; boundary=";

    private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";

    /**
     * 设置全局默认的连接和读取超时时长
     *
     * @param customTimeout 超时时长
     * @see HttpGlobalConfig#setTimeout(int)
     * @since 4.6.2
     */
    public static void setGlobalTimeout(int customTimeout) {
        HttpGlobalConfig.setTimeout(customTimeout);
    }

    /**
     * 获取Cookie管理器，用于自定义Cookie管理
     *
     * @return {@link CookieManager}
     * @see GlobalCookieManager#getCookieManager()
     * @since 4.1.0
     */
    public static CookieManager getCookieManager() {
        return GlobalCookieManager.getCookieManager();
    }

    /**
     * 自定义{@link CookieManager}
     *
     * @param customCookieManager 自定义的{@link CookieManager}
     * @see GlobalCookieManager#setCookieManager(CookieManager)
     * @since 4.5.14
     */
    public static void setCookieManager(CookieManager customCookieManager) {
        GlobalCookieManager.setCookieManager(customCookieManager);
    }

    /**
     * 关闭Cookie
     *
     * @see GlobalCookieManager#setCookieManager(CookieManager)
     * @since 4.1.9
     */
    public static void closeCookie() {
        GlobalCookieManager.setCookieManager(null);
    }

    private String url;

    private URLStreamHandler urlHandler;

    private Method method = Method.GET;

    /**
     * 默认连接超时
     */
    private int connectionTimeout = HttpGlobalConfig.getTimeout();

    /**
     * 默认读取超时
     */
    private int readTimeout = HttpGlobalConfig.getTimeout();

    /**
     * 存储表单数据
     */
    private Map<String, Object> form;

    /**
     * 文件表单对象，用于文件上传
     */
    private Map<String, Resource> fileForm;

    /**
     * Cookie
     */
    private String cookie;

    /**
     * 连接对象
     */
    private HttpConnection httpConnection;

    /**
     * 是否禁用缓存
     */
    private boolean isDisableCache;

    /**
     * 是否对url中的参数进行编码
     */
    private boolean encodeUrlParams;

    /**
     * 是否是REST请求模式
     */
    private boolean isRest;

    /**
     * 重定向次数计数器，内部使用
     */
    private int redirectCount;

    /**
     * 最大重定向次数
     */
    private int maxRedirectCount;

    /**
     * Chuncked块大小，0或小于0表示不设置Chuncked模式
     */
    private int blockSize;

    /**
     * 代理
     */
    private Proxy proxy;

    /**
     * HostnameVerifier，用于HTTPS安全连接
     */
    private HostnameVerifier hostnameVerifier;

    /**
     * SSLSocketFactory，用于HTTPS安全连接
     */
    private SSLSocketFactory ssf;

    // private UserService userService = UserService.getInstance();

    // /**
    // * 构造
    // *
    // * @param url URL
    // */
    // public LaniusDesignHttpRequest(String url) {
    // super(url);
    // this.header(Header.COOKIE, getAccessToken());
    // this.header(Header.USER_AGENT, DEF_AGENT);
    // this.header(Header.ACCEPT, DEF_ACCEPT);
    // this.header(Header.ACCEPT_ENCODING, DEF_ACCEPT_ENCODING);
    // this.header(Header.ACCEPT_LANGUAGE, DEF_ACCEPT_LANGUAGE);
    // this.header(Header.CACHE_CONTROL, DEF_CACHE_CONTROL);
    // this.header(HEADER_MACHINE_CODE, DEF_MACHINE_CODE);
    // this.header(HEADER_XREQUEST_WITH, DEF_XREQUEST_WITH);
    // this.header(HEADER_CHARSET, DEF_CHARSET);
    // }

    /**
     * 构造
     *
     * @param url URL
     */
    public HttpRequest(String url) {
        Assert.notBlank(url, "Param [url] can not be blank !");
        this.url = URLUtil.normalize(url, true);
        // 给定一个默认头信息
        header(GlobalHeaders.INSTANCE.headers());

        header(Header.USER_AGENT, HttpRequest.DEF_AGENT);
        // this.header(Header.ACCEPT, DEF_ACCEPT);
        // this.header(Header.ACCEPT_ENCODING, DEF_ACCEPT_ENCODING);
        // this.header(Header.ACCEPT_LANGUAGE, DEF_ACCEPT_LANGUAGE);
        header(Header.CACHE_CONTROL, HttpRequest.DEF_CACHE_CONTROL);
        header(HttpRequest.HEADER_MACHINE_CODE, HttpRequest.DEF_MACHINE_CODE);
        header(HttpRequest.HEADER_XREQUEST_WITH, HttpRequest.DEF_XREQUEST_WITH);
        header(HttpRequest.HEADER_CHARSET, HttpRequest.DEF_CHARSET);
        header(HttpRequest.HEADER_SYSTEM, HttpRequest.DEF_SYSTEM);
        // this.header(HEADER_ACCESS_TOKEN, userService.getAccessToken());
        header(HttpRequest.HEADER_LANIUS_VERSION, BuildVersion.getInstance().getVersion());
    }

    // ---------------------------------------------------------------- static Http
    // Method start

    /**
     * POST请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest post(String url) {
        return new HttpRequest(url).method(Method.POST);
    }

    /**
     * POST JSON请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest postJson(String url) {
        return new HttpRequest(url).method(Method.POST).contentType("application/json");
    }

    /**
     * POST JSON请求带请求头
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest postJsonWithAuth(String url, String token) {
        String[] nameValueArr = token.split(":");
        return new HttpRequest(url).method(Method.POST).contentType("application/json").header(nameValueArr[0], nameValueArr[1]);
    }

    /**
     * GET请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest get(String url) {
        return new HttpRequest(url).method(Method.GET);
    }

    /**
     * HEAD请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest head(String url) {
        return new HttpRequest(url).method(Method.HEAD);
    }

    /**
     * OPTIONS请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest options(String url) {
        return new HttpRequest(url).method(Method.OPTIONS);
    }

    /**
     * PUT请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest put(String url) {
        return new HttpRequest(url).method(Method.PUT);
    }

    /**
     * PATCH请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     * @since 3.0.9
     */
    public static HttpRequest patch(String url) {
        return new HttpRequest(url).method(Method.PATCH);
    }

    /**
     * DELETE请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest delete(String url) {
        return new HttpRequest(url).method(Method.DELETE);
    }

    /**
     * TRACE请求
     *
     * @param url URL
     * @return LaniusDesignHttpRequest
     */
    public static HttpRequest trace(String url) {
        return new HttpRequest(url).method(Method.TRACE);
    }
    // ---------------------------------------------------------------- static Http
    // Method end

    // ************************************************************* start

    // /**
    // * 获取用户口令
    // *
    // * @return
    // */
    // public String getAccessToken() {
    // return userService.getAccessToken();
    // }
    //
    // public void reloadAccessToken() {
    // this.header(HEADER_ACCESS_TOKEN, userService.getAccessToken());
    // }

    public int getReadTimeout() {
        return readTimeout;
    }

    public int getConnectionTimeout() {
        return connectionTimeout;
    }

    // ************************************************************* end

    /**
     * 获取请求URL
     *
     * @return URL字符串
     * @since 4.1.8
     */
    public String getUrl() {
        return url;
    }

    /**
     * 设置URL
     *
     * @param url url字符串
     * @return this
     * @since 4.1.8
     */
    public HttpRequest setUrl(String url) {
        this.url = url;
        return this;
    }

    /**
     * 设置{@link URLStreamHandler}
     * <p>
     * 部分环境下需要单独设置此项，例如当 WebLogic Server 实例充当 SSL 客户端角色（它会尝试通过 SSL
     * 连接到其他服务器或应用程序）时，<br>
     * 它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。如果主机名不匹配，则删除此连接。<br>
     * 因此weblogic不支持https的sni协议的主机名验证，此时需要将此值设置为sun.net.www.protocol.https.Handler对象。
     * <p>
     * 相关issue见：https://gitee.com/loolly/hutool/issues/IMD1X
     *
     * @param urlHandler {@link URLStreamHandler}
     * @return this
     * @since 4.1.9
     */
    public HttpRequest setUrlHandler(URLStreamHandler urlHandler) {
        this.urlHandler = urlHandler;
        return this;
    }

    /**
     * 获取Http请求方法
     *
     * @return {@link Method}
     * @since 4.1.8
     */
    public Method getMethod() {
        return method;
    }

    /**
     * 设置请求方法
     *
     * @param method HTTP方法
     * @return LaniusDesignHttpRequest
     * @see #method(Method)
     * @since 4.1.8
     */
    public HttpRequest setMethod(Method method) {
        return method(method);
    }

    /**
     * 获取{@link HttpConnection}<br>
     * 在{@link #execute()} 执行前此对象为null
     *
     * @return {@link HttpConnection}
     * @since 4.2.2
     */
    public HttpConnection getConnection() {
        return httpConnection;
    }

    /**
     * 设置请求方法
     *
     * @param method HTTP方法
     * @return LaniusDesignHttpRequest
     */
    public HttpRequest method(Method method) {
        if (Method.PATCH == method) {
            this.method = Method.POST;
            header("X-HTTP-Method-Override", "PATCH");
        } else {
            this.method = method;
        }

        return this;
    }

    // ---------------------------------------------------------------- Http Request
    // Header start

    /**
     * 设置contentType
     *
     * @param contentType contentType
     * @return LaniusDesignHttpRequest
     */
    public HttpRequest contentType(String contentType) {
        header(Header.CONTENT_TYPE, contentType);
        return this;
    }

    /**
     * 设置是否为长连接
     *
     * @param isKeepAlive 是否长连接
     * @return LaniusDesignHttpRequest
     */
    public HttpRequest keepAlive(boolean isKeepAlive) {
        header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
        return this;
    }

    /**
     * @return 获取是否为长连接
     */
    public boolean isKeepAlive() {
        String connection = header(Header.CONNECTION);
        if (connection == null) {
            return !httpVersion.equalsIgnoreCase(HttpBase.HTTP_1_0);
        }

        return false == "close".equalsIgnoreCase(connection);
    }

    /**
     * 获取内容长度
     *
     * @return String
     */
    public String contentLength() {
        return header(Header.CONTENT_LENGTH);
    }

    /**
     * 设置内容长度
     *
     * @param value 长度
     * @return LaniusDesignHttpRequest
     */
    public HttpRequest contentLength(int value) {
        header(Header.CONTENT_LENGTH, String.valueOf(value));
        return this;
    }

    /**
     * 设置Cookie<br>
     * 自定义Cookie后会覆盖Hutool的默认Cookie行为
     *
     * @param cookies Cookie值数组，如果为{@code null}则设置无效，使用默认Cookie行为
     * @return this
     * @since 3.1.1
     */
    public HttpRequest cookie(HttpCookie... cookies) {
        if (ArrayUtil.isEmpty(cookies)) {
            return disableCookie();
        }
        return cookie(ArrayUtil.join(cookies, ";"));
    }

    /**
     * 设置Cookie<br>
     * 自定义Cookie后会覆盖Hutool的默认Cookie行为
     *
     * @param cookie Cookie值，如果为{@code null}则设置无效，使用默认Cookie行为
     * @return this
     * @since 3.0.7
     */
    public HttpRequest cookie(String cookie) {
        this.cookie = cookie;
        return this;
    }

    /**
     * 禁用默认Cookie行为，此方法调用后会将Cookie置为空。<br>
     * 如果想重新启用Cookie，请调用：{@link #cookie(String)}方法自定义Cookie。<br>
     * 如果想启动默认的Cookie行为（自动回填服务器传回的Cookie），则调用{@link #enableDefaultCookie()}
     *
     * @return this
     * @since 3.0.7
     */
    public HttpRequest disableCookie() {
        return cookie(StrUtil.EMPTY);
    }

    /**
     * 打开默认的Cookie行为（自动回填服务器传回的Cookie）
     *
     * @return this
     */
    public HttpRequest enableDefaultCookie() {
        return cookie((String) null);
    }
    // ---------------------------------------------------------------- Http Request
    // Header end

    // ---------------------------------------------------------------- Form start

    /**
     * 设置表单数据<br>
     *
     * @param name  名
     * @param value 值
     * @return this
     */
    public HttpRequest form(String name, Object value) {
        if (StrUtil.isBlank(name) || ObjectUtil.isNull(value)) {
            return this; // 忽略非法的form表单项内容;
        }

        // 停用body
        bodyBytes = null;

        if (value instanceof File) {
            // 文件上传
            return form(name, (File) value);
        } else if (value instanceof Resource) {
            // 自定义流上传
            return form(name, (Resource) value);
        } else if (form == null) {
            form = new LinkedHashMap<>();
        }

        String strValue;
        if (value instanceof List) {
            // 列表对象
            strValue = CollectionUtil.join((List<?>) value, ",");
        } else if (ArrayUtil.isArray(value)) {
            if (File.class == ArrayUtil.getComponentType(value)) {
                // 多文件
                return form(name, (File[]) value);
            }
            // 数组对象
            strValue = ArrayUtil.join((Object[]) value, ",");
        } else {
            // 其他对象一律转换为字符串
            strValue = Convert.toStr(value, null);
        }

        form.put(name, strValue);
        return this;
    }

    /**
     * 设置表单数据
     *
     * @param name       名
     * @param value      值
     * @param parameters 参数对，奇数为名，偶数为值
     * @return this
     */
    public HttpRequest form(String name, Object value, Object... parameters) {
        form(name, value);

        for (int i = 0; i < parameters.length; i += 2) {
            name = parameters[i].toString();
            form(name, parameters[i + 1]);
        }
        return this;
    }

    /**
     * 设置map类型表单数据
     *
     * @param formMap 表单内容
     * @return this
     */
    public HttpRequest form(Map<String, Object> formMap) {
        if (MapUtil.isNotEmpty(formMap)) {
            for (Map.Entry<String, Object> entry : formMap.entrySet()) {
                form(entry.getKey(), entry.getValue());
            }
        }
        return this;
    }

    /**
     * 文件表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name  名
     * @param files 需要上传的文件
     * @return this
     */
    public HttpRequest form(String name, File... files) {
        if (1 == files.length) {
            final File file = files[0];
            return form(name, file, file.getName());
        }
        return form(name, new MultiFileResource(files));
    }

    /**
     * 文件表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name 名
     * @param file 需要上传的文件
     * @return this
     */
    public HttpRequest form(String name, File file) {
        return form(name, file, file.getName());
    }

    /**
     * 文件表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name     名
     * @param file     需要上传的文件
     * @param fileName 文件名，为空使用文件默认的文件名
     * @return this
     */
    public HttpRequest form(String name, File file, String fileName) {
        if (null != file) {
            form(name, new FileResource(file, fileName));
        }
        return this;
    }

    /**
     * 文件byte[]表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name      名
     * @param fileBytes 需要上传的文件
     * @param fileName  文件名
     * @return this
     * @since 4.1.0
     */
    public HttpRequest form(String name, byte[] fileBytes, String fileName) {
        if (null != fileBytes) {
            form(name, new BytesResource(fileBytes, fileName));
        }
        return this;
    }

    /**
     * 文件表单项<br>
     * 一旦有文件加入，表单变为multipart/form-data
     *
     * @param name     名
     * @param resource 数据源，文件可以使用{@link FileResource}包装使用
     * @return this
     * @since 4.0.9
     */
    public HttpRequest form(String name, Resource resource) {
        if (null != resource) {
            if (false == isKeepAlive()) {
                keepAlive(true);
            }

            if (null == fileForm) {
                fileForm = new HashMap<>();
            }
            // 文件对象
            fileForm.put(name, resource);
        }
        return this;
    }

    /**
     * 获取表单数据
     *
     * @return 表单Map
     */
    public Map<String, Object> form() {
        return form;
    }

    /**
     * 获取文件表单数据
     *
     * @return 文件表单Map
     * @since 3.3.0
     */
    public Map<String, Resource> fileForm() {
        return fileForm;
    }
    // ---------------------------------------------------------------- Form end

    // ---------------------------------------------------------------- Body start

    /**
     * 设置内容主体<br>
     * 请求体body参数支持两种类型：
     * <pre>
     * 1. 标准参数，例如 a=1&amp;b=2 这种格式
     * 2. Rest模式，此时body需要传入一个JSON或者XML字符串，Hutool会自动绑定其对应的Content-Type
     * </pre>
     *
     * @param body 请求体
     * @return this
     */
    public HttpRequest body(String body) {
        return body(body, null);
    }

    /**
     * 设置内容主体<br>
     * 请求体body参数支持两种类型：
     * <pre>
     * 1. 标准参数，例如 a=1&amp;b=2 这种格式
     * 2. Rest模式，此时body需要传入一个JSON或者XML字符串，Hutool会自动绑定其对应的Content-Type
     * </pre>
     *
     * @param body        请求体
     * @param contentType 请求体类型，{@code null}表示自动判断类型
     * @return this
     */
    public HttpRequest body(String body, String contentType) {
        byte[] bytes = StrUtil.bytes(body, charset);
        body(bytes);
        form = null; // 当使用body时，停止form的使用
        contentLength(bytes.length);

        if (null != contentType) {
            // Content-Type自定义设置
            contentType(contentType);
        } else {
            // 在用户未自定义的情况下自动根据内容判断
            contentType = HttpUtil.getContentTypeByRequestBody(body);
            if (null != contentType && ContentType.isDefault(header(Header.CONTENT_TYPE))) {
                if (null != charset) {
                    // 附加编码信息
                    contentType = ContentType.build(contentType, charset);
                }
                contentType(contentType);
            }
        }

        // 判断是否为rest请求
        if (StrUtil.containsAnyIgnoreCase(contentType, "json", "xml")) {
            isRest = true;
        }
        return this;
    }

    /**
     * 设置主体字节码<br>
     * 需在此方法调用前使用charset方法设置编码，否则使用默认编码UTF-8
     *
     * @param bodyBytes 主体
     * @return this
     */
    public HttpRequest body(byte[] bodyBytes) {
        if (null != bodyBytes) {
            this.bodyBytes = bodyBytes;
        }
        return this;
    }
    // ---------------------------------------------------------------- Body end

    /**
     * 设置超时，单位：毫秒<br>
     * 超时包括：
     * <pre>
     * 1. 连接超时
     * 2. 读取响应超时
     * </pre>
     *
     * @param milliseconds 超时毫秒数
     * @return this
     * @see #setConnectionTimeout(int)
     * @see #setReadTimeout(int)
     */
    public HttpRequest timeout(int milliseconds) {
        setConnectionTimeout(milliseconds);
        setReadTimeout(milliseconds);
        return this;
    }

    /**
     * 设置连接超时，单位：毫秒
     *
     * @param milliseconds 超时毫秒数
     * @return this
     * @since 4.5.6
     */
    public HttpRequest setConnectionTimeout(int milliseconds) {
        connectionTimeout = milliseconds;
        return this;
    }

    /**
     * 设置连接超时，单位：毫秒
     *
     * @param milliseconds 超时毫秒数
     * @return this
     * @since 4.5.6
     */
    public HttpRequest setReadTimeout(int milliseconds) {
        readTimeout = milliseconds;
        return this;
    }

    /**
     * 禁用缓存
     *
     * @return this
     */
    public HttpRequest disableCache() {
        isDisableCache = true;
        return this;
    }

    /**
     * 是否对URL中的参数进行编码
     *
     * @param isEncodeUrlParams 是否对URL中的参数进行编码
     * @return this
     * @since 4.4.1
     */
    public HttpRequest setEncodeUrlParams(boolean isEncodeUrlParams) {
        encodeUrlParams = isEncodeUrlParams;
        return this;
    }

    /**
     * 设置是否打开重定向，如果打开默认重定向次数为2<br>
     * 此方法效果与{@link #setMaxRedirectCount(int)} 一致
     *
     * @param isFollowRedirects 是否打开重定向
     * @return this
     */
    public HttpRequest setFollowRedirects(boolean isFollowRedirects) {
        return setMaxRedirectCount(isFollowRedirects ? 2 : 0);
    }

    /**
     * 设置最大重定向次数<br>
     * 如果次数小于1则表示不重定向，大于等于1表示打开重定向
     *
     * @param maxRedirectCount 最大重定向次数
     * @return this
     * @since 3.3.0
     */
    public HttpRequest setMaxRedirectCount(int maxRedirectCount) {
        this.maxRedirectCount = Math.max(maxRedirectCount, 0);
        return this;
    }

    /**
     * 设置域名验证器<br>
     * 只针对HTTPS请求，如果不设置，不做验证，所有域名被信任
     *
     * @param hostnameVerifier HostnameVerifier
     * @return this
     */
    public HttpRequest setHostnameVerifier(HostnameVerifier hostnameVerifier) {
        // 验证域
        this.hostnameVerifier = hostnameVerifier;
        return this;
    }

    /**
     * 设置代理
     *
     * @param proxy 代理 {@link Proxy}
     * @return this
     */
    public HttpRequest setProxy(Proxy proxy) {
        this.proxy = proxy;
        return this;
    }

    /**
     * 设置SSLSocketFactory<br>
     * 只针对HTTPS请求，如果不设置，使用默认的SSLSocketFactory<br>
     * 默认SSLSocketFactory为：SSLSocketFactoryBuilder.create().build();
     *
     * @param ssf SSLScketFactory
     * @return this
     */
    public HttpRequest setSSLSocketFactory(SSLSocketFactory ssf) {
        this.ssf = ssf;
        return this;
    }

    /**
     * 设置HTTPS安全连接协议，只针对HTTPS请求，可以使用的协议包括：<br>
     * 此方法调用后{@link #setSSLSocketFactory(SSLSocketFactory)} 将被覆盖。
     * <pre>
     * 1. TLSv1.2
     * 2. TLSv1.1
     * 3. SSLv3
     * ...
     * </pre>
     *
     * @param protocol 协议
     * @return this
     * @see SSLSocketFactoryBuilder
     * @see #setSSLSocketFactory(SSLSocketFactory)
     */
    public HttpRequest setSSLProtocol(String protocol) {
        Assert.notBlank(protocol, "protocol must be not blank!");
        try {
            ssf = SSLSocketFactoryBuilder.create().setProtocol(protocol).build();
        } catch (Exception e) {
            throw new HttpException(e);
        }
        return this;
    }

    /**
     * 设置是否rest模式<br>
     * rest模式下get请求不会把参数附加到URL之后
     *
     * @param isRest 是否rest模式
     * @return this
     * @since 4.5.0
     */
    public HttpRequest setRest(boolean isRest) {
        this.isRest = isRest;
        return this;
    }

    /**
     * 采用流方式上传数据，无需本地缓存数据。<br>
     * HttpUrlConnection默认是将所有数据读到本地缓存，然后再发送给服务器，这样上传大文件时就会导致内存溢出。
     *
     * @param blockSize 块大小（bytes数），0或小于0表示不设置Chuncked模式
     * @return this
     * @since 4.6.5
     */
    public HttpRequest setChunkedStreamingMode(int blockSize) {
        this.blockSize = blockSize;
        return this;
    }

    /**
     * 执行Reuqest请求
     *
     * @return this
     * @throws HttpException
     * @throws LaniusServerErrorException
     */
    public HttpResponse execute() throws LaniusServerErrorException {
        return execute(false);
    }

    /**
     * 异步请求<br>
     * 异步请求后获取的{@link HttpResponse} 为异步模式，执行完此方法后发送请求到服务器，但是并不立即读取响应内容。<br>
     * 此时保持Http连接不关闭，直调用获取内容方法为止。
     * <p>
     * 一般执行完execute之后会把响应内容全部读出来放在一个
     * byte数组里，如果你响应的内容太多内存就爆了，此法是发送完请求不直接读响应内容，等有需要的时候读。
     *
     * @return 异步对象，使用get方法获取HttpResponse对象
     * @throws HttpException
     * @throws LaniusServerErrorException
     */
    public HttpResponse executeAsync() throws LaniusServerErrorException, HttpException {
        return execute(true);
    }

    /**
     * 执行Reuqest请求
     *
     * @param isAsync 是否异步
     * @return this
     * @throws HttpException
     * @throws LaniusServerErrorException
     */
    public HttpResponse execute(boolean isAsync) throws LaniusServerErrorException {
        // 初始化URL
        urlWithParamIfGet();
        // 编码URL
        if (encodeUrlParams) {
            url = HttpUtil.encodeParams(url, charset);
        }
        // 初始化 connection
        initConnection();

        // 发送请求
        send();

        // 手动实现重定向
        HttpResponse httpResponse = sendRedirectIfPossible();

        // 获取响应
        if (null == httpResponse) {
            httpResponse = new HttpResponse(httpConnection, charset, isAsync, isIgnoreResponseBody());
        }

        checkResponse(httpResponse);
        return httpResponse;
    }

    /**
     * 简单验证
     *
     * @param username 用户名
     * @param password 密码
     * @return LaniusDesignHttpRequest
     */
    public HttpRequest basicAuth(String username, String password) {
        final String data = username.concat(":").concat(password);
        final String base64 = Base64.encode(data, charset);

        header("Authorization", "Basic " + base64, true);

        return this;
    }

    // ---------------------------------------------------------------- Private
    // method start

    private void checkResponse(HttpResponse response) throws LaniusServerErrorException, HttpException {
        if (!response.isOk()) {
            response.close();
            throw new LaniusServerErrorException(response.getStatus(), response.body());
        }
    }

    /**
     * 初始化网络连接
     */
    private void initConnection() {
        if (null != httpConnection) {
            // 执行下次请求时自动关闭上次请求（常用于转发）
            httpConnection.disconnectQuietly();
        }

        httpConnection = HttpConnection.create(URLUtil.toUrlForHttp(url, urlHandler), proxy)//
                .setMethod(method)//
                .setHttpsInfo(hostnameVerifier, ssf)//
                .setConnectTimeout(connectionTimeout)//
                .setReadTimeout(readTimeout)//
                // 定义转发
                .setInstanceFollowRedirects(maxRedirectCount > 0)
                // 流方式上传数据
                .setChunkedStreamingMode(blockSize)
                // 覆盖默认Header
                .header(headers, true);

        if (null != cookie) {
            // 当用户自定义Cookie时，全局Cookie自动失效
            httpConnection.setCookie(cookie);
        } else {
            // 读取全局Cookie信息并附带到请求中
            GlobalCookieManager.add(httpConnection);
        }

        // 是否禁用缓存
        if (isDisableCache) {
            httpConnection.disableCache();
        }
    }

    /**
     * 对于GET请求将参数加到URL中<br>
     * 此处不对URL中的特殊字符做单独编码
     */
    private void urlWithParamIfGet() {
        if (Method.GET.equals(method) && false == isRest) {
            // 优先使用body形式的参数，不存在使用form
            if (ArrayUtil.isNotEmpty(bodyBytes)) {
                url =
                        HttpUtil.urlWithForm(url, StrUtil.str(bodyBytes, charset), charset, false);
            } else {
                url = HttpUtil.urlWithForm(url, form, charset, false);
            }
        }
    }

    /**
     * 调用转发，如果需要转发返回转发结果，否则返回<code>null</code>
     *
     * @return {@link HttpResponse}，无转发返回 <code>null</code>
     * @throws HttpException
     * @throws LaniusServerErrorException
     */
    private HttpResponse sendRedirectIfPossible() throws LaniusServerErrorException {
        if (maxRedirectCount < 1) {
            // 不重定向
            return null;
        }

        // 手动实现重定向
        if (httpConnection.getHttpURLConnection().getInstanceFollowRedirects()) {
            int responseCode;
            try {
                responseCode = httpConnection.responseCode();
            } catch (IOException e) {
                // 错误时静默关闭连接
                httpConnection.disconnectQuietly();
                throw new HttpException(e);
            }
            if (responseCode != HttpURLConnection.HTTP_OK) {
                if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP
                        || responseCode == HttpURLConnection.HTTP_MOVED_PERM
                        || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
                    url = httpConnection.header(Header.LOCATION);
                    if (redirectCount < maxRedirectCount) {
                        redirectCount++;
                        return execute();
                    }
                }
            }
        }
        return null;
    }

    /**
     * 发送数据流
     *
     * @throws IORuntimeException IO异常
     */
    private void send() throws IORuntimeException {
        try {
            if (Method.POST.equals(method) //
                    || Method.PUT.equals(method) //
                    || Method.DELETE.equals(method) //
                    || isRest) {
                if (CollectionUtil.isEmpty(fileForm)) {
                    sendFormUrlEncoded();// 普通表单
                } else {
                    sendMultipart(); // 文件上传表单
                }
            } else {
                httpConnection.connect();
            }
        } catch (IOException e) {
            // 异常时关闭连接
            httpConnection.disconnectQuietly();
            throw new IORuntimeException(e);
        }
    }

    /**
     * 发送普通表单<br>
     * 发送数据后自动关闭输出流
     *
     * @throws IOException IO异常
     */
    private void sendFormUrlEncoded() throws IOException {
        if (StrUtil.isBlank(header(Header.CONTENT_TYPE))) {
            // 如果未自定义Content-Type，使用默认的application/x-www-form-urlencoded
            httpConnection.header(Header.CONTENT_TYPE, ContentType.FORM_URLENCODED.toString(charset), true);
        }

        // Write的时候会优先使用body中的内容，write时自动关闭OutputStream
        if (ArrayUtil.isNotEmpty(bodyBytes)) {
            IoUtil.write(httpConnection.getOutputStream(), true, bodyBytes);
        } else {
            final String content = HttpUtil.toParams(form, charset);
            IoUtil.write(httpConnection.getOutputStream(), charset, true, content);
        }
    }

    /**
     * 发送多组件请求（例如包含文件的表单）<br>
     * 发送数据后自动关闭输出流
     *
     * @throws IOException IO异常
     */
    private void sendMultipart() throws IOException {
        setMultipart();// 设置表单类型为Multipart

        try (OutputStream out = httpConnection.getOutputStream()) {
            writeFileForm(out);
            writeForm(out);
            formEnd(out);
        }
    }

    // 普通字符串数据

    /**
     * 发送普通表单内容
     *
     * @param out 输出流
     */
    private void writeForm(OutputStream out) {
        if (CollectionUtil.isNotEmpty(form)) {
            StringBuilder builder = StrUtil.builder();
            for (Entry<String, Object> entry : form.entrySet()) {
                builder.append("--").append(HttpRequest.BOUNDARY).append(StrUtil.CRLF);
                builder.append(StrUtil.format(HttpRequest.CONTENT_DISPOSITION_TEMPLATE, entry.getKey()));
                builder.append(entry.getValue()).append(StrUtil.CRLF);
            }
            IoUtil.write(out, charset, false, builder);
        }
    }

    /**
     * 发送文件对象表单
     *
     * @param out 输出流
     */
    private void writeFileForm(OutputStream out) {
        for (Entry<String, Resource> entry : fileForm.entrySet()) {
            appendPart(entry.getKey(), entry.getValue(), out);
        }
    }

    /**
     * 添加Multipart表单的数据项
     *
     * @param formFieldName 表单名
     * @param resource      资源，可以是文件等
     * @param out           Http流
     * @since 4.1.0
     */
    private void appendPart(String formFieldName, Resource resource, OutputStream out) {
        if (resource instanceof MultiResource) {
            // 多资源
            for (Resource subResource : (MultiResource) resource) {
                appendPart(formFieldName, subResource, out);
            }
        } else {
            // 普通资源
            final StringBuilder builder = StrUtil.builder().append("--").append(HttpRequest.BOUNDARY).append(StrUtil.CRLF);
            final String fileName = resource.getName();
            builder.append(StrUtil.format(HttpRequest.CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName,
                    ObjectUtil.defaultIfNull(fileName, formFieldName)));
            // 根据name的扩展名指定互联网媒体类型，默认二进制流数据
            builder.append(
                    StrUtil.format(HttpRequest.CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName, "application/octet-stream")));
            IoUtil.write(out, charset, false, builder);
            InputStream in = null;
            try {
                in = resource.getStream();
                IoUtil.copy(in, out);
            } finally {
                IoUtil.close(in);
            }
            IoUtil.write(out, charset, false, StrUtil.CRLF);
        }

    }

    // 添加结尾数据

    /**
     * 上传表单结束
     *
     * @param out 输出流
     * @throws IOException IO异常
     */
    private void formEnd(OutputStream out) throws IOException {
        out.write(HttpRequest.BOUNDARY_END);
        out.flush();
    }

    /**
     * 设置表单类型为Multipart（文件上传）
     */
    private void setMultipart() {
        httpConnection.header(Header.CONTENT_TYPE, HttpRequest.CONTENT_TYPE_MULTIPART_PREFIX + HttpRequest.BOUNDARY, true);
    }

    /**
     * 是否忽略读取响应body部分<br>
     * HEAD、CONNECT、OPTIONS、TRACE方法将不读取响应体
     *
     * @return 是否需要忽略响应body部分
     * @since 3.1.2
     */
    private boolean isIgnoreResponseBody() {
        return Method.HEAD == method //
                || Method.CONNECT == method //
                || Method.OPTIONS == method //
                || Method.TRACE == method;
    }
    // ---------------------------------------------------------------- Private
    // method end

}
